/*
 * Install using:
 *    sudo apt-get install nodejs
 *    sudo apt-get install npm
 *    
 *    sudo npm install stylus -g
 */
var async = require('async'),
  crypto = require('crypto'),
  express = require('express'),
  fs = require('fs'),
  http = require('http'),
  moment = require('moment'),
  mysql = require('mysql'),
  path = require('path'),
  temp = require('temp');

var app = express();

var connection = mysql.createConnection({
  database : 'wespodu',
  host     : 'localhost',
  user     : 'root',
  password : 'fenomen',
});

connection.connect();

app.use(express.cookieParser());
app.use(express.session({secret: 'neuveriteltajemnavec'}));

app.configure(function(){
  app.set('port', process.env.PORT || 8090);
  app.set('views', __dirname + '/public/html');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(require('stylus').middleware(__dirname + '/public'));
  app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function(){
  app.use(express.errorHandler());
});

/**
 * Serve static content from public/ directory.
 */
app.get('/', function(req, res) {
  res.render('index', {
    title: 'Home'
  });
});

/**
 * Return information needed by user.
 */
app.get('/content', function(req, res) {
  if (typeof req.session.login !== 'number') {
    return res.send({'error': 'incorrect_login'});
  }
  var subjects = {};
  async.series([
    function(callback) {
      connection.query('SELECT ' +
                         'idsubject, ' +
                         'name ' +
                       'FROM subject NATURAL JOIN enroll ' +
                       'WHERE iduser = ?',
                       [req.session.login],
                       function(err, rows, fields) {
        if (err) {
          console.log(err);
          return callback('database_error');
        }
        var len = rows.length;
        for (var i = 0; i < len; ++i) {
          var id = '' + rows[i].idsubject;
          var name = rows[i].name;
          subjects[id] = {'name' : name,
                          'tests': {}};
        }
        callback();
      });
    },
    function(callback) {
      var ids = [];
      for (var i in subjects) {
        ids.push(i);
      }
      async.each(ids, function(id, callbackInner) {
        connection.query('SELECT ' +
                           'active, ' +
                           'idtest, ' +
                           'name, ' +
                           'comp_comm, ' +
                           'run_comm, ' +
                           'DATE_FORMAT(deadline, GET_FORMAT(DATETIME, "EUR")) AS time ' +
                         'FROM test ' +
                         'WHERE idsubject = ?',
                         [id],
                         function(err, rows, fields) {
          if (err) {
            console.log(err);
            return callbackInner('database_error');
          }
          var tests = {};
          var len = rows.length;
          for (var i = 0; i < len; ++i) {
            var active = rows[i].active === 1;
            var subjectId = '' + rows[i].idtest;
            var name = rows[i].name;
            var compileCommand = rows[i].comp_comm;
            var runCommand = rows[i].run_comm;
            var deadline = rows[i].time.replace(/(....)-(..)-(..) (..)\.(..)\.(..)/,
                                                '$3. $2. $1 $4:$5:$6');
            tests[subjectId] = {'active': active,
                                'name': name,
                                'compile_command': compileCommand,
                                'run_command': runCommand,
                                'deadline': deadline,
                                'running': {},
                                'required_files': {}};
          }
          subjects[id].tests = tests;
          callbackInner();
        });
      }, callback);
    },
    function(callback) {
      var ids = [];
      for (var i in subjects) {
        for (var j in subjects[i].tests){
          ids.push({'subjectId': i,
                    'testId': j});
        }
      }
      async.each(ids, function(id, callbackInner) {
        var selectStudent = 'SELECT ' +
                           'idrunned_test, ' +
                           'result, ' +
                           'DATE_FORMAT(time, GET_FORMAT(DATETIME, "EUR")) AS time ' +
                         'FROM runned_test ' +
                         'WHERE idtest = ? AND iduser = ?';
        var selectTeacher = 'SELECT ' +
                             'idrunned_test, ' +
                             'result, ' +
                             'DATE_FORMAT(time, GET_FORMAT(DATETIME, "EUR")) AS time, ' +
                             'login ' +
                             'FROM runned_test ' +
                             'NATURAL JOIN user ' +
                             'WHERE idtest=?;'
        connection.query(isLoggedAsTeacher(req) ? selectTeacher : selectStudent,
                         [id.testId, req.session.login],
                         function(err, rows, fields) {
          if (err) {
            console.log(err);
            return callbackInner('database_error');
          }
          var running = {};
          var len = rows.length;
          for (var i = 0; i < len; ++i) {
            var runningId = '' + rows[i].idrunned_test;
            var result = rows[i].result !== 0;
            var student = rows[i].login;
            var filename = rows[i].file;
            var time = rows[i].time.replace(/(....)-(..)-(..) (..)\.(..)\.(..)/,
                                            '$3. $2. $1 $4:$5:$6');
            running[runningId] = {'done': result,
                                  'submitted': time,
                                  'student': student,
                                  'messages': {}};
          }
          subjects[id.subjectId].
            tests[id.testId].
            running = running;
          callbackInner();
        });
      }, callback);
    },
    function(callback) {
      var ids = [];
      for (var i in subjects) {
        for (var j in subjects[i].tests) {
        ids.push({'subjectId': i,
                  'testId': j});
        }
      }
      async.each(ids, function(id, callbackInner) {
        connection.query('SELECT ' +
                         'idreq_file, ' +
                         'file, ' +
                         'DATE_FORMAT(time, GET_FORMAT(DATETIME, "EUR")) AS time ' +
                         'FROM req_file ' +
                         'WHERE idtest = ?',
                         [id.testId],
                         function(err, rows, fields) {
          if (err) {
            console.log(err);
            return callbackInner('database_error');
          }
          var requiredFiles = {};
          var len = rows.length;
          for (var i = 0; i < len; ++i) {
            var requiredFileId = '' + rows[i].idreq_file;
            var name = rows[i].file;
            var time = rows[i].time.replace(/(....)-(..)-(..) (..)\.(..)\.(..)/,
                                            '$3. $2. $1 $4:$5:$6');
            requiredFiles[requiredFileId] = {'filename': name,
                                             'submitted': time};
          }
          subjects[id.subjectId].
            tests[id.testId].
            required_files = requiredFiles;
          callbackInner();
        });
      }, callback);
    },
    function(callback) {
      var ids = [];
      for (var i in subjects) {
        for (var j in subjects[i].tests) {
          for (var k in subjects[i].tests[j].running) {
            ids.push({'subjectId': i,
                      'testId': j,
                      'runningId': k});
          }
        }
      }
      async.each(ids, function(id, callbackInner) {
        connection.query('SELECT ' +
                           'login, ' +
                           'idmessage, ' +
                           'content, ' +
                           'DATE_FORMAT(time, GET_FORMAT(DATETIME, "EUR")) AS time ' +
                         'FROM message NATURAL JOIN user ' +
                         'WHERE idrunned_test = ?',
                         [id.runningId],
                         function(err, rows, fields) {
          if (err) {
            console.log(err);
            return callbackInner('database_error');
          }
          var messages = {};
          var len = rows.length;
          for (var i = 0; i < len; ++i) {
            var messageId = '' + rows[i].idmessage;
            var content = rows[i].content;
            var from = rows[i].login;
            var time = rows[i].time.replace(/(....)-(..)-(..) (..)\.(..)\.(..)/,
                                            '$3. $2. $1 $4:$5:$6');
            messages[messageId] = {'content': content,
                                   'from': from,
                                   'time': time};
          }
          subjects[id.subjectId].
            tests[id.testId].
            running[id.runningId].
            messages = messages;
          callbackInner();
        });
      }, callback);
    }
  ], function(err) {
    res.send(err ? {'error': err} : subjects);
  });
});

/**
 * Log the user in.
 */
app.post('/login', function(req, res) {
  if(typeof req.body.name !== 'string' ||
     typeof req.body.password !== 'string' ||
     req.body.name.length < 5 ||
     req.body.password.length < 5) {
    res.send({'error': 'incorrect_login'});
    return;
  }
  var shasum = crypto.createHash('sha512');
  for (var i = 0; i < 42; ++i) {
    shasum.update(req.body.password);
  }
  connection.query('SELECT iduser, role FROM user WHERE login = ? AND password = ?',
                   [req.body.name, shasum.digest('hex')],
                   function(err, rows, fields) {
    if (err || rows.length !== 1) {
      res.send({'error': 'incorrect_login'});
      return;
    }
    req.session.login = ~~rows[0].iduser;
    req.session.isStudent = req.body.name[0] === 'x';
    res.send({'id': JSON.stringify(rows[0].iduser),
              'role': JSON.stringify(rows[0].role)});
  });
});

/**
 * Log the user out.
 */
app.post('/logout', function(req, res) {
  delete req.session.login;
  res.send({});
});

/**
 * Add new task.
 */
app.post('/add_test', function(req, res) {
  if (!isLoggedAsTeacher(req)) {
    return res.send({'error': 'incorrect_login'});
  }
  var subjectId = req.body.subjectId;
  var name = req.body.name;
  var compileCommand = req.body.compileCommand;
  var runCommand = req.body.runCommand;
  var solutionFilename = req.body.solutionFilename;
  var deadline = req.body.deadline;
  if (typeof subjectId !== 'string' ||
      typeof name !== 'string' || !name.length ||
      typeof compileCommand !== 'string' || !compileCommand.length ||
      typeof runCommand !== 'string' || !runCommand.length ||
      typeof deadline !== 'string' || !deadline.length ||
      typeof solutionFilename !== 'string' || !solutionFilename.length) {
    return res.send({'error': 'wrong_parameter'});
  }
  connection.query('INSERT INTO test (' + 
                      'idsubject, name, comp_comm, run_comm, deadline, ' +
                      'solution_filename' + 
                   ') VALUES (?, ?, ?, ?, ?, ?)',
                   [subjectId, name, compileCommand, runCommand, deadline, solutionFilename],
                   function(err, rows, fields) {
    if (err) {
      console.log(err);
      return res.send({'error': 'database_error'});
    }
    connection.query('SELECT idtest FROM test WHERE name = ?',
                     [name],
                     function(err1, rows1, fields1) {
      if (err1 || rows1.length !== 1) {
        console.log(err1);
        return res.send({'error': 'database_error'});
      }
      res.send({});
    });
  });
});

/**
 * Add file to task.
 */
app.post('/add_file', function(req, res) {
  if (!isLoggedAsTeacher(req)) {
    return res.send({'error': 'incorrect_login'});
  }
  var filename = req.body.filename;
  var testId = ~~req.body.testId;
  var submitted = req.body.submitted;
  if (typeof filename !== 'string' || !filename.length ||
      typeof testId !== 'number' || testId <= 0 ||
      !Object.keys(req.files).length) {
    return res.send({'error': 'wrong_parameter'});
  }
  connection.query('DELETE FROM req_file WHERE idtest = ? AND file = ?',
                   [testId, filename],
                   function(err0, rows0, fields0) {
    connection.query('INSERT INTO req_file(' + 
                        'idtest, file, time' + 
                     ') VALUES (?, ?, ?)',
                     [testId, filename, submitted],
                     function(err1, rows1, fields1) {
      if (err1) {
        console.log(err1);
        return res.send({'error': 'database_error'});
      }
      var filepath = path.normalize(__dirname + '/tests/' + testId + '/' + filename);
      fs.mkdir(path.normalize(__dirname + '/tests/' + testId), function() {
        fs.unlink(filepath, function() {
          copyFile(req.files[Object.keys(req.files)[0]].path,
                   filepath,
                   function(err) {
            if (err) return res.send({'error': 'upload_failed'});
            res.send({});
          });
        });
      });
    });
  });
});

/**
 * Activate task, making it visible to students and removing any possibility to
 * alter the task.
 */
app.post('/activate_test', function(req, res) {
  if (!isLoggedAsTeacher(req)) {
    return res.send({'error': 'incorrect_login'});
  }
  var testId = ~~req.body.testId;
  if (typeof testId !== 'number' || testId < 0) {
    return res.send({'error': 'wrong_parameter'});
  }
  connection.query('UPDATE test SET active = 1 WHERE idtest = ?',
                   [testId],
                   function(err, rows, fields) {
    if (err) {
      console.log(err);
      return res.send({'error': 'database_error'});
    }
    res.send({});
  });
});

/**
 * Upload solution and run it.
 */
app.post('/run_test', function(req, res) {
  if (!isLoggedAsStudent(req)) {
    return res.send({'error': 'incorrect_login'});
  }
  console.log(req.body);
  var testId = ~~req.body.testId;
  var time = moment().format('YYYY-MM-DD HH:mm:ss');
  if (typeof testId !== 'number' || testId <= 0 ||
      !Object.keys(req.files).length) {
    return res.send({'error': 'wrong_parameter'});
  }
  connection.query('SELECT COUNT(*) as rows ' +
                   'FROM runned_test ' +
                   'WHERE result = 1 AND idtest = ?',
                   [testId],
                   function(err, rows) {
    if (err || rows.length !== 1) {
      console.log(err);
      return res.send({'error': 'database_error'});
    }
    if (rows[0].rows) return res.send({'error': 'already_done'});
    connection.query('SELECT comp_comm, run_comm, deadline, active, solution_filename ' +
                     'FROM test ' +
                     'WHERE idtest = ?',
                     [testId],
                     function(err, rows) {
      if (err || rows.length !== 1) {
        console.log(err);
        return res.send({'error': 'database_error'});
      }
      var compileCommandParameters = rows[0].comp_comm.split(' ');
      var compileCommand = compileCommandParameters.shift();
      var runningCommandParameters = rows[0].run_comm.split(' ');
      var runningCommand = runningCommandParameters.shift();
      var directoryTest = path.normalize(__dirname + '/solutions/' + testId + '/');
      var directoryTestInner = path.normalize(directoryTest + req.session.login + '/');
      var solutionFilename = rows[0].solution_filename;
      var filepath = path.normalize(directoryTestInner + solutionFilename);
      var deadline = rows[0].deadline;
      var active = rows[0].active === 1;
      if (time > deadline) return res.send({'error': 'after_deadline'});
      if (!active) return res.send({'error': 'not_active'});
      fs.mkdir(directoryTest, function(err) {
        fs.mkdir(directoryTestInner, function(err) {
          fs.unlink(filepath, function(err) {
            copyFile(req.files[Object.keys(req.files)[0]].path,
                     filepath,
                     function(err) {
              if (err) {
                console.log(err);
                return res.send({'error': 'upload_failed'});
              }
              prepare(testId, solutionFilename, function(err) {
                if (err) {
                  console.log(err);
                  return res.send({'error': 'upload_failed'});
                }
                var compilation = require('child_process').execFile(
                    compileCommand,
                    compileCommandParameters,
                    function(err, stdout, stderr) {
                  if (err) return storeResult('not_compiled');
                  var compilation = require('child_process').
                      execFile(runningCommand,
                               runningCommandParameters,
                               function(err, stdout, stderr) {
                    if (err) return storeResult('test_failed');
                    storeResult();
                  });
                });
              });
            });
          });
        });
      });
    });
  });
  
  var prepare = function(testId, solutionFilename, callback) {
    connection.query('SELECT file FROM req_file WHERE idtest = ?',
                     [testId],
                     function(err, rows) {
      if (err) return callback(err);
      var filenames = [];
      var len = rows.length;
      if (!len) return callback(err);
      for (var i = 0; i < len; ++i) {
        filenames.push(rows[i].file);
      }
      prepareFiles(testId, filenames, solutionFilename, callback);
    });
  };
  
  var storeResult = function(error) {
    connection.query('INSERT INTO runned_test (' +
                       'idtest, iduser, result) VALUES (?, ?, ?)',
                     [testId, req.session.login, error ? 0 : 1],
                     function(err) {
      if (err) {
        console.log(err);
        return res.send({'error': 'database_error'});
      }
      if (error) {
        console.log(error);
        return res.send({'error': error});
      }
      console.log('Solution ok.');
      res.send({});
    });
  };
  
  var prepareFiles = function(testId, filenames, solutionFilename, callback) {
    temp.mkdir('temp', function(err, dirPath) {
      if (err) return callback(err);
      async.each(filenames, function(filename, callbackInner) {
        copyFile(path.normalize(__dirname + '/tests/' + testId + '/' + filename),
                 path.normalize(dirPath + '/' + filename),
                 callbackInner);
      }, function(err) {
        if (err) return callback(err);
        copyFile(path.normalize(__dirname + '/solutions/' + testId + '/' +
                                req.session.login + '/' + solutionFilename),
                 path.normalize(dirPath + '/' + solutionFilename),
                 function(err) {
          if (err) {
            console.log(err);
            return callback(err);
          }
          process.chdir(dirPath);
          callback();
        });
      });
    });
  };
});

/**
 * Comment solution.
 */
app.post('/add_message', function(req, res) {
  if (!isLoggedAsTeacher(req)) {
    return res.send({'error': 'incorrect_login'});
  }
  var content = req.body.content;
  var runningTestId = req.body.runningTestId;
  var time = moment().format('YYYY-MM-DD HH:mm:ss');
  if (typeof content !== 'string' || !content.length) {
    return res.send({'error': 'wrong_parameter'});
  }
  connection.query('INSERT INTO message(' + 
                      'idrunned_test, iduser, time, content' + 
                   ') VALUES (?, ?, ?, ?)',
                   [runningTestId, req.session.login, time, content],
                   function(err) {
    if (err) {
      console.log(err);
      return res.send({'error': 'database_error'});
    }
    res.send({});
  });
});

/**
 * Returns true when user is currently logged as teacher.
 * 
 * @param {object} req request object
 */
function isLoggedAsTeacher(req) {
  return typeof req.session.login === 'number' &&
         typeof req.session.isStudent === 'boolean' &&
         !req.session.isStudent;
}

/**
 * Returns true when user is currently logged as student.
 * 
 * @param {object} req request object
 */
function isLoggedAsStudent(req) {
  return typeof req.session.login === 'number' &&
         typeof req.session.isStudent === 'boolean' &&
         req.session.isStudent;
}

/**
 * Copies file.
 * 
 * @param {string} source path
 * @param {string} target path
 * @param {function?} callback
 */
function copyFile(source, target, callback) {
  var callbackCalled = false;

  var rd = fs.createReadStream(source);
  rd.on('error', function(err) {
    done(err);
  });
  var wr = fs.createWriteStream(target, {'mode': 0777});
  wr.on('error', function(err) {
    done(err);
  });
  wr.on('close', function(ex) {
    done();
  });
  rd.pipe(wr);

  function done(err) {
    if (!callbackCalled) {
      callbackCalled = true;
      callback(err);
    }
  }
}

http.createServer(app).listen(app.get('port'), function() {
  console.log('Express server listening on port ' + app.get('port'));
});
